/*  
 * Actipro Software's WPF Docking & MDI / Prism Integration Add-on
 * http://actipro.codeplex.com/
 * 
 * Copyright (c) 2009-2015 Actipro Software LLC. 
 *  
 * This source code is subject to the terms of the Microsoft Public License (Ms-PL). 
 *  
 * Redistribution and use in source and binary forms, with or without modification, 
 * is permitted provided that redistributions of the source code retain the above 
 * copyright notices and this file header. 
 *  
 * Additional copyright notices should be appended to the list above. 
 * 
 * For details, see <http://www.opensource.org/licenses/ms-pl.html>. 
 *  
 * All other rights reserved. 
 *
 */

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Windows;
using ActiproSoftware.Products.Docking.Interop.Prism;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Prism.Regions;
using PrismRegionManager = Microsoft.Practices.Prism.Regions.RegionManager;

namespace ActiproSoftware.Windows.Prism.Regions {
	
	/// <summary>
	/// Represents a region for use with the <see cref="DockSiteRegionAdapter"/>.
	/// </summary>
	public sealed class DockSiteRegion : ObservableObjectBase, INavigateAsync, IRegion {

		private DockSiteRegionViewsCollection		activeViews;
		private IRegionBehaviorCollection			behaviors;
		private object								context;
		private bool								isAddingItem;
		private ObservableCollection<ItemMetadata>	itemMetadataCollection;
		private string								name;
		private IRegionNavigationService			navigationService;
		private IRegionManager						regionManager;
		private List<ItemMetadata>					removeItemsQueue;
		private Comparison<object>					sortComparison;
		private DockSiteRegionViewsCollection		views;

		/////////////////////////////////////////////////////////////////////////////////////////////////////
		#region OBJECT
		/////////////////////////////////////////////////////////////////////////////////////////////////////

		/// <summary>
		/// Initializes a new instance of the <see cref="DockSiteRegion"/> class.
		/// </summary>
		public DockSiteRegion() {
			this.sortComparison = Region.DefaultSortComparison;
		}

		#endregion // OBJECT

		/////////////////////////////////////////////////////////////////////////////////////////////////////
		#region INTERFACE IMPLEMENTATION
		/////////////////////////////////////////////////////////////////////////////////////////////////////

		#region INavigateAsync Members

		/// <summary>
		/// Initiates navigation to the target specified by the <see cref="Uri"/>.
		/// </summary>
		/// <param name="target">The navigation target</param>
		/// <param name="navigationCallback">The callback executed when the navigation request is completed.</param>
		/// <remarks>
		/// Convenience overloads for this method can be found as extension methods on the
		/// <see cref="NavigationAsyncExtensions"/> class.
		/// </remarks>
		public void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback) {
			this.NavigationService.RequestNavigate(target, navigationCallback);
		}
		
		/// <summary>
		/// Initiates navigation to the target specified by the <see cref="Uri"/>.
		/// </summary>
		/// <param name="target">The navigation target</param>
		/// <param name="navigationCallback">The callback executed when the navigation request is completed.</param>
		/// <param name="navigationParameters">The navigation parameters.</param>
		/// <remarks>
		/// Convenience overloads for this method can be found as extension methods on the
		/// <see cref="NavigationAsyncExtensions"/> class.
		/// </remarks>
		public void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters) {
			this.NavigationService.RequestNavigate(target, navigationCallback, navigationParameters);
		}

		#endregion // INavigateAsync Members

		#region IRegion Members

		/// <summary>
		/// Marks the specified view as active.
		/// </summary>
		/// <param name="view">The view to activate.</param>
		public void Activate(object view) {
			ItemMetadata itemMetadata = this.GetItemMetadata(view, true);
			if (!itemMetadata.IsActive)
				itemMetadata.IsActive = true;
		}

		/// <summary>
		/// Gets a read-only view of the collection of all the active views in the region.
		/// </summary>
		/// <value>An <see cref="IViewsCollection"/> of all the active views.</value>
		public IViewsCollection ActiveViews {
			get {
				if (this.activeViews == null)
					this.activeViews = new DockSiteRegionViewsCollection(this.ItemMetadataCollection, x => !x.IsActive) {
						SortComparison = this.sortComparison,
					};
				return this.activeViews;
			}
		}

		/// <summary>
		/// Adds a new view to the region.
		/// </summary>
		/// <param name="view">The view to add.</param>
		/// <returns>
		/// The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>. It will be the current region manager when using this overload.
		/// </returns>
		/// <overloads>Adds a new view to the region.</overloads>
		public IRegionManager Add(object view) {
			return this.Add(view, null, false);
		}

		/// <summary>
		/// Adds a new view to the region.
		/// </summary>
		/// <param name="view">The view to add.</param>
		/// <param name="viewName">The name of the view. This can be used to retrieve it later by calling <see cref="GetView"/>.</param>
		/// <returns>
		/// The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>. It will be the current region manager when using this overload.
		/// </returns>
		public IRegionManager Add(object view, string viewName) {
			return this.Add(view, viewName, false);
		}

		/// <summary>
		/// Adds a new view to the region.
		/// </summary>
		/// <param name="view">The view to add.</param>
		/// <param name="viewName">The name of the view. This can be used to retrieve it later by calling <see cref="GetView"/>.</param>
		/// <param name="createRegionManagerScope">When <see langword="true"/>, the added view will receive a new instance of <see cref="IRegionManager"/>, otherwise it will use the current region manager for this region.</param>
		/// <returns>
		/// The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>.
		/// </returns>
		public IRegionManager Add(object view, string viewName, bool createRegionManagerScope) {
			IRegionManager manager = (createRegionManagerScope) ? this.RegionManager.CreateRegionManager() : this.RegionManager;
			this.AddInternal(view, viewName, manager);
			return manager;
		}

		/// <summary>
		/// Gets the collection of <see cref="IRegionBehavior"/>s that can extend the behavior of regions.
		/// </summary>
		/// <value></value>
		public IRegionBehaviorCollection Behaviors {
			get {
				if (this.behaviors == null)
					this.behaviors = new RegionBehaviorCollection(this);
				return this.behaviors;
			}
		}

		/// <summary>
		/// Gets or sets a context for the region. This value can be used by the user to share context with the views.
		/// </summary>
		/// <value>The context value to be shared.</value>
		public object Context {
			get {
				return this.context;
			}
			set {
				if (this.context != value) {
					this.context = value;
					this.NotifyPropertyChanged("Context");
				}
			}
		}

		/// <summary>
		/// Marks the specified view as inactive.
		/// </summary>
		/// <param name="view">The view to deactivate.</param>
		public void Deactivate(object view) {
			ItemMetadata itemMetadata = this.GetItemMetadata(view, false);
			if (itemMetadata != null && itemMetadata.IsActive)
				itemMetadata.IsActive = false;
		}

		/// <summary>
		/// Returns the view instance that was added to the region using a specific name.
		/// </summary>
		/// <param name="viewName">The name used when adding the view to the region.</param>
		/// <returns>
		/// Returns the named view or <see langword="null"/> if the view with <paramref name="viewName"/> does not exist in the current region.
		/// </returns>
		public object GetView(string viewName) {
			if (string.IsNullOrEmpty(viewName))
				throw new ArgumentException(SR.GetString(SRName.ExStringCannotBeNullOrEmpty), "viewName");

			ItemMetadata metadata = this.ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName);
			return (metadata != null) ? metadata.Item : null;
		}

		/// <summary>
		/// Gets the name of the region that uniquely identifies the region within a <see cref="IRegionManager"/>.
		/// </summary>
		/// <value>The name of the region.</value>
		public string Name {
			get {
				return this.name;
			}
			set {
				if (this.name != null && this.name != value)
					throw new InvalidOperationException(SR.GetString(SRName.ExRegionNameCannotBeChanged));
				else if (string.IsNullOrEmpty(value))
					throw new ArgumentException(SR.GetString(SRName.ExStringCannotBeNullOrEmpty), "value");

				this.name = value;
				this.NotifyPropertyChanged("Name");
			}
		}

		/// <summary>
		/// Gets or sets the <see cref="IRegionManager"/> that will be passed to the views when adding them to the region, unless the view is added by specifying createRegionManagerScope as <see langword="true"/>.
		/// </summary>
		/// <value>
		/// The <see cref="IRegionManager"/> where this <see cref="IRegion"/> is registered.
		/// </value>
		/// <remarks>This is usually used by implementations of <see cref="IRegionManager"/> and should not be used by the developer explicitly.</remarks>
		public IRegionManager RegionManager {
			get {
				return this.regionManager;
			}
			set {
				if (this.regionManager != value) {
					this.regionManager = value;
					this.NotifyPropertyChanged("RegionManager");
				}
			}
		}

		/// <summary>
		/// Removes the specified view from the region.
		/// </summary>
		/// <param name="view">The view to remove.</param>
		public void Remove(object view) {
			// Get the associated metadata and remove it
			ItemMetadata itemMetadata = this.GetItemMetadata(view, false);
			if (itemMetadata != null) {
				// 1/31/2011 - If an item is currently be added, then the item must be queued for removal (008-14D91EC3-CFCC)
				if (this.isAddingItem) {
					if (this.removeItemsQueue == null)
						this.removeItemsQueue = new List<ItemMetadata>();
					this.removeItemsQueue.Add(itemMetadata);
				}
				else {
					this.ItemMetadataCollection.Remove(itemMetadata);
				}
			}

			// Clear the region manager attached property, if needed
			DependencyObject depObj = view as DependencyObject;
			if (depObj != null && PrismRegionManager.GetRegionManager(depObj) == this.RegionManager)
				depObj.ClearValue(PrismRegionManager.RegionManagerProperty);
		}

		/// <summary>
		/// Gets or sets the comparison used to sort the views.
		/// </summary>
		/// <value>The comparison to use.</value>
		public Comparison<object> SortComparison {
			get {
				return this.sortComparison;
			}
			set {
				if (this.sortComparison != value) {
					this.sortComparison = value;
					if (this.activeViews != null)
						this.activeViews.SortComparison = this.sortComparison;
					if (this.views != null)
						this.views.SortComparison = this.sortComparison;
					this.NotifyPropertyChanged("SortComparison");
				}
			}
		}

		/// <summary>
		/// Gets a readonly view of the collection of views in the region.
		/// </summary>
		/// <value>An <see cref="IViewsCollection"/> of all the added views.</value>
		public IViewsCollection Views {
			get {
				if (this.views == null)
					this.views = new DockSiteRegionViewsCollection(this.ItemMetadataCollection, null) {
						SortComparison = this.sortComparison,
					};
				return this.views;
			}
		}

		#endregion // IRegion Members

		#endregion // INTERFACE IMPLEMENTATION

		/////////////////////////////////////////////////////////////////////////////////////////////////////
		#region NON-PUBLIC PROCEDURES
		/////////////////////////////////////////////////////////////////////////////////////////////////////

		/// <summary>
		/// Adds the specified view to the region.
		/// </summary>
		/// <param name="view">The view to add.</param>
		/// <param name="viewName">Name of the view.</param>
		/// <param name="scopedRegionManager">The scoped region manager.</param>
		private void AddInternal(object view, string viewName, IRegionManager scopedRegionManager) {
			if (this.ItemMetadataCollection.Any(x => x.Item == view))
				throw new InvalidOperationException(SR.GetString(SRName.ExRegionViewAlreadyExists));

			// Create the metadata and save the view name
			ItemMetadata itemMetadata = new ItemMetadata(view);
			if (!string.IsNullOrEmpty(viewName)) {
				if (this.ItemMetadataCollection.Any(x => x.Name == viewName))
					throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, SR.GetString(SRName.ExRegionViewNameAlreadyExists), viewName));
				itemMetadata.Name = viewName;
			}

			// Set the RegionManager.RegionManagerProperty attached property
			DependencyObject dependencyObject = view as DependencyObject;
			if (dependencyObject != null)
				PrismRegionManager.SetRegionManager(dependencyObject, scopedRegionManager);

			// Add to the metadata collection
			try {
				this.isAddingItem = true;
				this.ItemMetadataCollection.Add(itemMetadata);
			}
			finally {
				this.isAddingItem = false;

				// Determine if any items were queued for removal, if so then remove them
				if (this.removeItemsQueue != null) {
					foreach (ItemMetadata metadata in removeItemsQueue)
						this.ItemMetadataCollection.Remove(metadata);
					this.removeItemsQueue = null;
				}
			}
		}

		/// <summary>
		/// Gets the item metadata for the specified view.
		/// </summary>
		/// <param name="view">The view.</param>
		/// <param name="throwIfMissing">if <c>true</c> then an exception will be thrown if the view cannot be found.</param>
		/// <returns>The item metadata for the specified view.</returns>
		private ItemMetadata GetItemMetadata(object view, bool throwIfMissing) {
			if (view == null)
				throw new ArgumentNullException("view");

			ItemMetadata itemMetadata = this.ItemMetadataCollection.FirstOrDefault(x => x.Item == view);
			if (itemMetadata == null && throwIfMissing)
				throw new ArgumentException(SR.GetString(SRName.ExRegionViewNotFound), "view");

			return itemMetadata;
		}

		/// <summary>
		/// Gets the list of metadata, and their views, associated with this region.
		/// </summary>
		/// <value>The list of metadata, and their views, associated with this region.</value>
		private ObservableCollection<ItemMetadata> ItemMetadataCollection {
			get {
				if (this.itemMetadataCollection == null)
					this.itemMetadataCollection = new ObservableCollection<ItemMetadata>();
				return this.itemMetadataCollection;
			}
		}

		#endregion // NON-PUBLIC PROCEDURES

		/////////////////////////////////////////////////////////////////////////////////////////////////////
		#region PUBLIC PROCEDURES
		/////////////////////////////////////////////////////////////////////////////////////////////////////

		/// <summary>
		/// Gets or sets the associated <see cref="IRegionNavigationService"/> service.
		/// </summary>
		/// <value>The navigation service.</value>
		public IRegionNavigationService NavigationService {
			get {
				if (this.navigationService == null) {
					this.navigationService = ServiceLocator.Current.GetInstance<IRegionNavigationService>();
					this.navigationService.Region = this;
				}
				return this.navigationService;
			}
			set {
				this.navigationService = value;
			}
		}

		#endregion // PUBLIC PROCEDURES
	}
}